﻿/*
 *  This file is part of SXAPI.
 *
 *  SXAPI is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, version 3 of the License.

 *  SXAPI is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with SXAPI.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using Jayrock.Json;
using System.IO;
using System.Xml;
using System.Globalization;
using System.Collections.ObjectModel;

namespace StackExchangeApi
{
    /// <summary>
    /// Represents a question on one of the "Trilogy Sites"
    /// </summary>
    public sealed class Question : Post
    {
        private Object m_set_post_sync = new Object();

        [BacksCache]
        private volatile bool m_favorite_set = false;
        private long m_favorite_count;

        [BacksCache]
        private volatile bool m_answer_count_set = false;
        private long m_answer_count;

        [BacksCache]
        private volatile bool m_views_set = false;
        private long m_views;

        [BacksCache]
        private volatile ReadOnlyCollection<String> m_tags;

        [BacksCache]
        private volatile bool m_created_set = false;
        private DateTime m_created = DateTime.MinValue;

        [BacksCache]
        private volatile bool m_editted_set = false;
        private DateTime m_editted = DateTime.MinValue;

        [BacksCache]
        private volatile ReadOnlyCollection<Answer> m_answers;

        /// <summary>
        /// Converts an EagerQuestion into a Question.
        /// 
        /// Although the Question would normally be lazily-loaded,
        /// since it is initialized with a fully populated EagerQuestion
        /// no loading at all will occur when its properties are
        /// accessed.
        /// </summary>
        /// <param name="question">EagerQuestion to cast</param>
        /// <returns>A Question as above.</returns>
        public static implicit operator Question(EagerQuestion question)
        {
            return QuestionCache.GetOrAllocate(question.Id, question.Site);
        }

        /// <summary>
        /// Number of users who have favorited this question
        /// </summary>
        public long FavoriteCount
        {
            get
            {
                if (!m_favorite_set)
                {
                    lock (m_set_post_sync)
                    {
                        if (!m_favorite_set)
                        {
                            SetPostValues(this);
                        }
                    }
                }

                return m_favorite_count;
            }

            internal set
            {
                m_favorite_count = value;
                m_favorite_set = true;
            }
        }

        /// <summary>
        /// Number of answers to this question
        /// </summary>
        public long AnswerCount
        {
            get
            {
                if (!m_answer_count_set)
                {
                    lock (m_set_post_sync)
                    {
                        if (!m_answer_count_set)
                        {
                            SetPostValues(this);
                        }
                    }
                }

                return m_answer_count;
            }
            internal set
            {
                m_answer_count = value;
                m_answer_count_set = true;
            }
        }

        /// <summary>
        /// Number of views on this question
        /// </summary>
        public long ViewCount
        {
            get
            {
                if (!m_views_set)
                {
                    lock (m_set_post_sync)
                    {
                        if (!m_views_set)
                            SetPostValues(this);
                    }
                }

                return m_views;
            }
            internal set
            {
                m_views = value;
                m_views_set = true;
            }
        }

        /// <summary>
        /// Tags on this question
        /// </summary>
        public ReadOnlyCollection<String> Tags
        {
            get
            {
                if (m_tags == null)
                {
                    lock (m_set_post_sync)
                    {
                        if (m_tags == null)
                            SetPostValues(this);
                    }
                }

                return m_tags;
            }
            internal set
            {
                m_tags = value;
            }
        }

        /// <summary>
        /// Date this question was created
        /// </summary>
        public DateTime Created
        {
            get
            {
                if (!m_created_set)
                {
                    lock (m_set_post_sync)
                    {
                        if (!m_created_set)
                        {
                            SetPostValues(this);
                        }
                    }
                }

                return m_created;
            }

            internal set
            {
                m_created = value;
                m_created_set = true;
            }
        }

        /// <summary>
        /// Date this question was last editted
        /// </summary>
        public DateTime LastEdit
        {
            get
            {
                if (!m_editted_set)
                {
                    lock (m_set_post_sync)
                    {
                        if (!m_editted_set)
                        {
                            SetPostValues(this);
                        }
                    }
                }

                return m_editted;
            }

            internal set
            {
                m_editted = value;
                m_editted_set = true;
            }
        }

        /// <summary>
        /// The answers for this question
        /// </summary>
        public ReadOnlyCollection<Answer> Answers
        {
            get
            {
                if (m_answers == null)
                {
                    lock (m_set_post_sync)
                    {
                        if (m_answers == null)
                        {
                            SetPostValues(this);
                        }
                    }
                }

                return m_answers;
            }

            internal set
            {
                m_answers = value;
                if (m_answers != null)
                    AnswerCount = m_answers.Count;
            }
        }

        /// <summary>
        /// Construct a new Question
        /// </summary>
        /// <param name="id">id of the question</param>
        /// <param name="site">Site the quesiton is on</param>
        internal Question(long id, StackExchangeSite site) : base(id, site) { }

        /// <summary>
        /// Create a new Question.
        /// 
        /// Note that this will succeed even if the question does not exist.
        /// </summary>
        /// <param name="id">id of the question in... question</param>
        /// <param name="site">Site the question is on</param>
        /// <returns>A new Question, ready for queries</returns>
        public static Question GetQuestionWithId(long id, StackExchangeSite site)
        {
            return QuestionCache.GetOrAllocate(id, site);
        }

        /// <summary>
        /// Sets the Questions property on the given User.
        /// </summary>
        /// <param name="user">The user in question</param>
        internal static void SetQuestionsForUser(User user)
        {
            DelayedChunkProvider<Question>
                questionsProvider =
                delegate(int chunk, out int chunkCount)
                {
                    List<Question> questionsOnPage = new List<Question>();

                    String str = ConnectionMediator.MakeRequest(GetUserQuestionsUri(user, chunk + 1));
                    JsonReader reader = new JsonTextReader(new StringReader(str));

                    JsonObject obj = new JsonObject();
                    obj.Import(reader);

                    JsonArray posts = (JsonArray)obj["Posts"];

                    for (int i = 0; i < posts.Length; i++)
                    {
                        JsonObject post = (JsonObject)posts[i];

                        long id = ((JsonNumber)post["Id"]).ToInt64();
                        long favs = ((JsonNumber)post["FavCount"]).ToInt64();
                        long votes = ((JsonNumber)post["VoteCount"]).ToInt64();
                        long answers = ((JsonNumber)post["AnswerCount"]).ToInt64();
                        long views = ((JsonNumber)post["ViewCount"]).ToInt64();
                        String title = (String)post["Title"];
                        List<String> tags = new List<String>(((String)post["Tags"]).Replace(@"\u003c", " ").Replace(@"\u003e", "").Split(' '));

                        long createdTicks = 0;
                        if (!(post["CreatedDate"] is JsonNull) && post["CreatedDate"] != null)
                        {
                            String create = (String)post["CreatedDate"];
                            int a = create.IndexOf('(') + 1;
                            int b = create.IndexOf(')');
                            String cStr = create.Substring(a, b - a);

                            createdTicks = Int64.Parse(cStr, CultureInfo.InvariantCulture);
                        }

                        long lastEditTicks = 0;
                        if (!(post["LastEditDate"] is JsonNull) && post["LastEditDate"] != null)
                        {
                            String edit = (String)post["LastEditDate"];
                            int a = edit.IndexOf('(') + 1;
                            int b = edit.IndexOf(')');
                            String eStr = edit.Substring(a, b - a);

                            lastEditTicks = Int64.Parse(eStr, CultureInfo.InvariantCulture);
                        }

                        DateTime created = DateTime.MinValue;
                        if (createdTicks != 0)
                            created = new DateTime(createdTicks);


                        DateTime lastEdit = DateTime.MinValue;
                        if (lastEditTicks != 0)
                            lastEdit = new DateTime(lastEditTicks);

                        Question question = Question.GetQuestionWithId(id, user.Site);
                        question.ViewCount = views;
                        question.Created = created;
                        question.LastEdit = lastEdit;
                        question.Tags = tags.AsReadOnly();
                        question.AnswerCount = answers;
                        question.FavoriteCount = favs;
                        question.Title = title;
                        question.Votes = votes;

                        questionsOnPage.Add(question);
                    }

                    chunkCount = ((JsonNumber)obj["Total"]).ToInt32() / 50;

                    return questionsOnPage;
                };

            user.Questions = new ReadOnlyDelayLoadedList<Question>(new DelayLoadedList<Question>(questionsProvider, 50));
        }

        /// <summary>
        /// Gets recent questions that have the given tags from the given site.
        /// </summary>
        /// <param name="site">The site in question.</param>
        /// <param name="tags">At least one tag</param>
        /// <returns>As above.</returns>
        public static ICollection<Question> GetRecentQuestionsWithTags(StackExchangeSite site, params String[] tags)
        {
            if (tags.Length == 0)
                throw new ArgumentException("Must provide at least one tag");

            List<Question> questions = new List<Question>();

            String rss = ConnectionMediator.MakeRequest(GetRecentQuestionsWithTagsUri(site, tags));

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(rss);

            XmlNode root = doc.LastChild;

            XmlNode entry = null;

            foreach (XmlNode node in root.ChildNodes)
                if (node.Name == "entry")
                {
                    entry = node;
                    break;
                }

            do
            {
                Question q = null;
                SetValues(site, entry, ref q, false);

                questions.Add(q);
            } while ((entry = entry.NextSibling) != null);

            return questions;
        }

        /// <summary>
        /// Construct a URI that when fetched returns an RSS feed of recent questions
        /// with the given tags.
        /// </summary>
        /// <param name="site">The site to query</param>
        /// <param name="tags">An array of at least one tag</param>
        /// <returns>As above</returns>
        private static Uri GetRecentQuestionsWithTagsUri(StackExchangeSite site, String[] tags)
        {
            for(int i = 0; i < tags.Length; i++)
                tags[i] = " "+tags[i];

            String tagStr = String.Concat(tags).Trim();
            //Bringing System.Web into the project purely for this method may be a mistake
            tagStr = System.Web.HttpUtility.UrlEncode(tagStr);

            String str = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/feeds/tag/{1}",
                Utils.GetAddressForSite(site),
                tagStr);  

            return new Uri(str);
        }

        /// <summary>
        /// Get the most recent questions from a "Trilogy Site."
        /// 
        /// The number of questions returned is determined by the site.
        /// </summary>
        /// <param name="site">Site in question</param>
        /// <returns>A list of the recent questions</returns>
        public static ICollection<Question> GetRecentQuestions(StackExchangeSite site)
        {
            List<Question> questions = new List<Question>();

            String rss = ConnectionMediator.MakeRequest(GetRecentQuestionsUri(site));

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(rss);

            XmlNode root = doc.LastChild;

            XmlNode entry = null;

            foreach (XmlNode node in root.ChildNodes)
                if (node.Name == "entry")
                {
                    entry = node;
                    break;
                }

            
            do
            {
                Question q = null;

                SetValues(site, entry, ref q, false);

                questions.Add(q);
            } while ((entry = entry.NextSibling) != null);

            return questions;
        }

        /// <summary>
        /// Builds a URI that when fetched will return an RSS feed with recent questions
        /// from the given site.
        /// </summary>
        /// <param name="site">The site in question</param>
        /// <returns>As above</returns>
        private static Uri GetRecentQuestionsUri(StackExchangeSite site)
        {
            String str = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/feeds", 
                Utils.GetAddressForSite(site));

            return new Uri(str);
        }

        /// <summary>
        /// Build a uri that returns a given page of the questions a user has asked
        /// </summary>
        /// <param name="user">The user in question</param>
        /// <param name="pageNum">the page number to fetch</param>
        /// <returns>a Uri as above</returns>
        private static Uri GetUserQuestionsUri(User user, int pageNum)
        {
            String str = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/api/userquestions.json?userid={1}&page={2}&pagesize={3}&sort={4}",
                Utils.GetAddressForSite(user.Site),
                user.Id,
                pageNum,
                50,
                "recent");

            return new Uri(str);
        }

        /// <summary>
        /// Parses the values of a Question out of an "entry" entry in
        /// the question RSS feed.
        /// </summary>
        /// <param name="site">Site this question is on</param>
        /// <param name="entry">Entry to parse</param>
        /// <param name="question">Question to populate</param>
        /// <param name="expectAnswers">Whether or not answers will be in this entry feed</param>
        internal static void SetValues(StackExchangeSite site, XmlNode entry, ref Question question, bool expectAnswers)
        {
            Dictionary<String, String> vals = new Dictionary<string, string>();
            List<Answer> answers = null;

            foreach (XmlNode node in entry)
            {
                if (node.Name == "category")
                {
                    String tag = (String)node.Attributes["term"].InnerText;

                    if (!vals.Keys.Contains("tags"))
                    {
                        vals["tags"] = tag;
                    }
                    else
                    {
                        vals["tags"] = vals["tags"] + " " + tag;
                    }

                    continue;
                }

                Answer.ParseValues(entry, vals);
            }

            if (expectAnswers)
            {
                answers = new List<Answer>();
                while (entry.NextSibling != null)
                {
                    Dictionary<String, String> anses = new Dictionary<string, string>();
                    entry = entry.NextSibling;

                    Answer.ParseValues(entry, anses);

                    String id = anses["id"];
                    int p = id.LastIndexOf("#", StringComparison.Ordinal);
                    id = id.Substring(p + 1);
                    anses["id"] = id;

                    Answer answer = Answer.GetAnswerWithId(Int64.Parse(anses["id"], CultureInfo.InvariantCulture), question.Site);
                    answer.Title = anses["title"];
                    answer.Votes = Int64.Parse(anses["re:rank"], CultureInfo.InvariantCulture);

                    answers.Add(answer);
                }
            }

            if (question == null)
            {
                String idStr = vals["id"];
                int i = idStr.IndexOf("questions/", StringComparison.Ordinal);
                i += 10;
                int j = idStr.IndexOf("/", i, StringComparison.Ordinal);
                idStr = idStr.Substring(i, j - i);

                question = new Question(Int64.Parse(idStr, CultureInfo.InvariantCulture), site);
            }

            question.Author = vals["author"];

            question.Created = DateTime.ParseExact(vals["published"], "u", DateTimeFormatInfo.InvariantInfo);
            question.LastEdit = DateTime.ParseExact(vals["updated"], "u", DateTimeFormatInfo.InvariantInfo);
            question.Summary = vals["summary"];
            question.Tags = new List<String>(vals["tags"].Split(' ')).AsReadOnly();

            //TODO:  Figure out a way to grab ViewCount without too much hackery
            question.ViewCount = 0;

            //TODO:  Likewise for FavoriteCount
            question.FavoriteCount = 0;

            if (answers != null)
                question.Answers = answers.AsReadOnly();

            //Defined in Post
            question.Title = vals["title"];
            question.Votes = Int64.Parse(vals["re:rank"], CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Load values into a question
        /// </summary>
        /// <param name="other">A Question to populate</param>
        internal override void SetPostValues(Post other)
        {
            Question question = (Question)other;
            String xml = ConnectionMediator.MakeRequest(GetQuestionValuesUri(question.Id, question.Site));

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xml);

            XmlNode root = doc.LastChild;

            XmlNode entry = null;

            foreach (XmlNode node in root.ChildNodes)
                if (node.Name == "entry")
                {
                    entry = node;
                    break;
                }

            SetValues(other.Site, entry, ref question, true);
        }

        /// <summary>
        /// Constructs a uri that gets the RSS feed for a given question id.
        /// </summary>
        /// <param name="id">Id of the Question</param>
        /// <param name="site">Site the question exists on</param>
        /// <returns>As above</returns>
        private static Uri GetQuestionValuesUri(long id, StackExchangeSite site)
        {
            String str = String.Format(
                CultureInfo.InvariantCulture,
                "http://{0}/feeds/question/{1}", 
                Utils.GetAddressForSite(site), 
                id);

            return new Uri(str);
        }

        /// <summary>
        /// Custom Equality operation.
        /// 
        /// Note that it IS possible for an EagerQuestion to be
        /// equivalent to a Question.
        /// </summary>
        /// <param name="obj">To test for equality</param>
        /// <returns>true if equal, false otherwise</returns>
        public override bool Equals(object obj)
        {
            Question otherQ = obj as Question;
            EagerQuestion otherEQ = obj as EagerQuestion;

            if (otherQ != null)
            {
                return otherQ.Site.Equals(Site) &&
                       otherQ.Id.Equals(Id);
            }

            if (otherEQ != null)
            {
                return otherEQ.Site.Equals(Site) &&
                       otherEQ.Id.Equals(Id);
            }

            return false;
        }

        /// <summary>
        /// Custom hash-code implementation
        /// </summary>
        /// <returns>A hashcode for this Question</returns>
        public override int GetHashCode()
        {
            return Id.GetHashCode() ^
                   (-1 * Site.GetHashCode());
        }

        #if DEBUG
        /// <summary>
        /// Overridden for debugging purposes only.
        /// </summary>
        /// <returns>A string representation of the Question</returns>
        public override string ToString()
        {
            return "[" + Title + " @" + ViewCount + " +" + Votes + "]";
        }
        #endif
    }

    /// <summary>
    /// Class for managing the internal cache of all
    /// allocated Questions.
    /// 
    /// Used so that allocating new Questions doesn't result in
    /// duplicate requests to populate it.
    /// </summary>
    internal sealed class QuestionCache : AllocationCache<Question> { }
}
